package docs

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"
	"text/template"

	"github.com/Jeffail/benthos/v3/lib/util/config"
	"github.com/Jeffail/gabs/v2"
	"gopkg.in/yaml.v3"
)

// AnnotatedExample is an isolated example for a component.
type AnnotatedExample struct {
	// A title for the example.
	Title string

	// Summary of the example.
	Summary string

	// A config snippet to show.
	Config string
}

// Status of a component.
type Status string

// Component statuses.
var (
	StatusStable       Status = "stable"
	StatusBeta         Status = "beta"
	StatusExperimental Status = "experimental"
	StatusDeprecated   Status = "deprecated"
)

// ComponentSpec describes a Benthos component.
type ComponentSpec struct {
	// Name of the component
	Name string

	// Type of the component (input, output, etc)
	Type string

	// The status of the component.
	Status Status

	// Summary of the component (in markdown, must be short).
	Summary string

	// Description of the component (in markdown).
	Description string

	// Categories that describe the purpose of the component.
	Categories []string

	// Footnotes of the component (in markdown).
	Footnotes string

	// Examples demonstrating use cases for the component.
	Examples []AnnotatedExample

	Fields FieldSpecs

	// Version is the Benthos version this component was introduced.
	Version string
}

type fieldContext struct {
	Name             string
	Type             string
	Description      string
	Default          string
	Advanced         bool
	Deprecated       bool
	Interpolation    FieldInterpolation
	Examples         []string
	AnnotatedOptions [][2]string
	Options          []string
	Version          string
}

type componentContext struct {
	Name               string
	Type               string
	FrontMatterSummary string
	Summary            string
	Description        string
	Categories         string
	Examples           []AnnotatedExample
	Fields             []fieldContext
	Footnotes          string
	CommonConfig       string
	AdvancedConfig     string
	Status             string
	Version            string
}

func (ctx fieldContext) InterpolationBatchWide() FieldInterpolation {
	return FieldInterpolationBatchWide
}

func (ctx fieldContext) InterpolationIndividual() FieldInterpolation {
	return FieldInterpolationIndividual
}

var componentTemplate = `{{define "field_docs" -}}
## Fields

{{range $i, $field := .Fields -}}
### ` + "`{{$field.Name}}`" + `

{{$field.Description}}
{{if eq $field.Interpolation .InterpolationBatchWide -}}
This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries).
{{end -}}
{{if eq $field.Interpolation .InterpolationIndividual -}}
This field supports [interpolation functions](/docs/configuration/interpolation#bloblang-queries).
{{end}}

Type: ` + "`{{$field.Type}}`" + `  
{{if gt (len $field.Default) 0}}Default: ` + "`{{$field.Default}}`" + `  
{{end -}}
{{if gt (len $field.Version) 0}}Requires version {{$field.Version}} or newer  
{{end -}}
{{if gt (len $field.AnnotatedOptions) 0}}
| Option | Summary |
|---|---|
{{range $j, $option := $field.AnnotatedOptions -}}` + "| `" + `{{index $option 0}}` + "` |" + ` {{index $option 1}} |
{{end}}
{{else if gt (len $field.Options) 0}}Options: {{range $j, $option := $field.Options -}}
{{if ne $j 0}}, {{end}}` + "`" + `{{$option}}` + "`" + `{{end}}.
{{end}}
{{if gt (len $field.Examples) 0 -}}
` + "```yaml" + `
# Examples

{{range $j, $example := $field.Examples -}}
{{if ne $j 0}}
{{end}}{{$example}}{{end -}}
` + "```" + `

{{end -}}
{{end -}}
{{end -}}

---
title: {{.Name}}
type: {{.Type}}
status: {{.Status}}
{{if gt (len .FrontMatterSummary) 0 -}}
description: "{{.FrontMatterSummary}}"
{{end -}}
{{if gt (len .Categories) 0 -}}
categories: {{.Categories}}
{{end -}}
---

<!--
     THIS FILE IS AUTOGENERATED!

     To make changes please edit the contents of:
     lib/{{.Type}}/{{.Name}}.go
-->

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

{{if eq .Status "beta" -}}
BETA: This component is mostly stable but breaking changes could still be made outside of major version releases if a fundamental problem with the component is found.
{{end -}}
{{if eq .Status "experimental" -}}
EXPERIMENTAL: This component is experimental and therefore subject to change or removal outside of major version releases.
{{end -}}
{{if eq .Status "deprecated" -}}
:::warning DEPRECATED
This component is deprecated and will be removed in the next major version release. Please consider moving onto [alternative components](#alternatives).
:::
{{end -}}

{{if gt (len .Summary) 0 -}}
{{.Summary}}
{{end -}}{{if gt (len .Version) 0}}
Introduced in version {{.Version}}.
{{end}}
{{if eq .CommonConfig .AdvancedConfig -}}
` + "```yaml" + `
# Config fields, showing default values
{{.CommonConfig -}}
` + "```" + `
{{else}}
<Tabs defaultValue="common" values={{"{"}}[
  { label: 'Common', value: 'common', },
  { label: 'Advanced', value: 'advanced', },
]{{"}"}}>

<TabItem value="common">

` + "```yaml" + `
# Common config fields, showing default values
{{.CommonConfig -}}
` + "```" + `

</TabItem>
<TabItem value="advanced">

` + "```yaml" + `
# All config fields, showing default values
{{.AdvancedConfig -}}
` + "```" + `

</TabItem>
</Tabs>
{{end -}}
{{if gt (len .Description) 0}}
{{.Description}}
{{end}}
{{if and (le (len .Fields) 4) (gt (len .Fields) 0) -}}
{{template "field_docs" . -}}
{{end -}}

{{if gt (len .Examples) 0 -}}
## Examples

<Tabs defaultValue="{{ (index .Examples 0).Title }}" values={{"{"}}[
{{range $i, $example := .Examples -}}
  { label: '{{$example.Title}}', value: '{{$example.Title}}', },
{{end -}}
]{{"}"}}>

{{range $i, $example := .Examples -}}
<TabItem value="{{$example.Title}}">

{{if gt (len $example.Summary) 0 -}}
{{$example.Summary}}
{{end}}
{{if gt (len $example.Config) 0 -}}
` + "```yaml" + `{{$example.Config}}` + "```" + `
{{end}}
</TabItem>
{{end -}}
</Tabs>

{{end -}}

{{if gt (len .Fields) 4 -}}
{{template "field_docs" . -}}
{{end -}}

{{if gt (len .Footnotes) 0 -}}
{{.Footnotes}}
{{end}}
`

func (c *ComponentSpec) createConfigs(root string, fullConfigExample interface{}) (
	advancedConfigBytes, commonConfigBytes []byte,
) {
	rootArray, isRootArray := fullConfigExample.([]interface{})
	isEmptyRootArray := isRootArray && len(rootArray) == 0

	var err error
	if len(c.Fields) > 0 && !isEmptyRootArray {
		advancedConfig, err := c.Fields.ConfigAdvanced(fullConfigExample)
		if err == nil {
			tmp := map[string]interface{}{
				c.Name: advancedConfig,
			}
			if len(root) > 0 {
				tmp = map[string]interface{}{
					root: tmp,
				}
			}
			advancedConfigBytes, err = config.MarshalYAML(tmp)
		}
		var commonConfig interface{}
		if err == nil {
			commonConfig, err = c.Fields.ConfigCommon(advancedConfig)
		}
		if err == nil {
			tmp := map[string]interface{}{
				c.Name: commonConfig,
			}
			if len(root) > 0 {
				tmp = map[string]interface{}{
					root: tmp,
				}
			}
			commonConfigBytes, err = config.MarshalYAML(tmp)
		}
	}
	if err != nil {
		panic(err)
	}
	if isEmptyRootArray || len(c.Fields) == 0 {
		tmp := map[string]interface{}{
			c.Name: fullConfigExample,
		}
		if len(root) > 0 {
			tmp = map[string]interface{}{
				root: tmp,
			}
		}
		if advancedConfigBytes, err = config.MarshalYAML(tmp); err != nil {
			panic(err)
		}
		commonConfigBytes = advancedConfigBytes
	}
	return
}

// AsMarkdown renders the spec of a component, along with a full configuration
// example, into a markdown document.
func (c *ComponentSpec) AsMarkdown(nest bool, fullConfigExample interface{}) ([]byte, error) {
	if strings.Contains(c.Summary, "\n\n") {
		return nil, fmt.Errorf("%v component '%v' has a summary containing empty lines", c.Type, c.Name)
	}

	ctx := componentContext{
		Name:        c.Name,
		Type:        c.Type,
		Summary:     c.Summary,
		Description: c.Description,
		Examples:    c.Examples,
		Footnotes:   c.Footnotes,
		Status:      string(c.Status),
		Version:     c.Version,
	}
	if len(ctx.Status) == 0 {
		ctx.Status = string(StatusStable)
	}

	if len(c.Categories) > 0 {
		cats, _ := json.Marshal(c.Categories)
		ctx.Categories = string(cats)
	}

	if tmpBytes, err := yaml.Marshal(fullConfigExample); err == nil {
		fullConfigExample = map[string]interface{}{}
		if err = yaml.Unmarshal(tmpBytes, &fullConfigExample); err != nil {
			panic(err)
		}
	} else {
		panic(err)
	}

	root := ""
	if nest {
		root = c.Type
	}

	advancedConfigBytes, commonConfigBytes := c.createConfigs(root, fullConfigExample)
	ctx.CommonConfig = string(commonConfigBytes)
	ctx.AdvancedConfig = string(advancedConfigBytes)

	gConf := gabs.Wrap(fullConfigExample)

	if len(c.Description) > 0 && c.Description[0] == '\n' {
		ctx.Description = c.Description[1:]
	}
	if len(c.Footnotes) > 0 && c.Footnotes[0] == '\n' {
		ctx.Footnotes = c.Footnotes[1:]
	}

	flattenedFields := FieldSpecs{}
	var walkFields func(path string, gObj *gabs.Container, f FieldSpecs) ([]string, []string)
	walkFields = func(path string, gObj *gabs.Container, f FieldSpecs) ([]string, []string) {
		var missingFields []string
		expectedFields := map[string]struct{}{}
		for k := range gObj.ChildrenMap() {
			expectedFields[k] = struct{}{}
		}
		seenFields := map[string]struct{}{}
		var duplicateFields []string
		for _, v := range f {
			if _, seen := seenFields[v.Name]; seen {
				duplicateFields = append(duplicateFields, v.Name)
			}
			seenFields[v.Name] = struct{}{}
			newV := v
			delete(expectedFields, v.Name)
			if len(path) > 0 {
				newV.Name = path + newV.Name
			}
			flattenedFields = append(flattenedFields, newV)
			if len(v.Children) > 0 {
				newPath := path + v.Name
				if newV.Type == FieldArray {
					newPath = newPath + "[]"
				}
				tmpMissing, tmpDuplicate := walkFields(newPath+".", gConf.S(v.Name), v.Children)
				missingFields = append(missingFields, tmpMissing...)
				duplicateFields = append(duplicateFields, tmpDuplicate...)
			}
		}
		for k := range expectedFields {
			missingFields = append(missingFields, path+k)
		}
		return missingFields, duplicateFields
	}
	if len(c.Fields) > 0 {
		rootPath := ""
		if _, isArray := gConf.Data().([]interface{}); isArray {
			rootPath = "[]."
		}
		if missing, duplicates := walkFields(rootPath, gConf, c.Fields); len(missing) > 0 {
			return nil, fmt.Errorf("spec missing fields: %v", missing)
		} else if len(duplicates) > 0 {
			return nil, fmt.Errorf("spec duplicate fields: %v", duplicates)
		}
	}

	for _, v := range flattenedFields {
		if v.Deprecated {
			continue
		}

		// TODO: Find another way to verify array element fields
		if !strings.Contains(v.Name, "[].") && !gConf.ExistsP(v.Name) {
			return nil, fmt.Errorf("unrecognised field '%v'", v.Name)
		}

		defaultValue := v.Default
		if defaultValue == nil {
			defaultValue = gConf.Path(v.Name).Data()
		}
		if defaultValue == nil {
			return nil, fmt.Errorf("field '%v' not found in config example", v.Name)
		}

		defaultValueStr := gabs.Wrap(defaultValue).String()
		if len(v.Children) > 0 {
			defaultValueStr = ""
		}

		fieldType := v.Type
		if len(fieldType) == 0 {
			if len(v.Examples) > 0 {
				fieldType = GetFieldType(v.Examples[0])
			} else {
				fieldType = GetFieldType(defaultValue)
			}
		}

		var examples []string
		if len(v.Examples) > 0 {
			nameSplit := strings.Split(v.Name, ".")
			exampleName := nameSplit[len(nameSplit)-1]
			for _, example := range v.Examples {
				exampleBytes, err := config.MarshalYAML(map[string]interface{}{
					exampleName: example,
				})
				if err != nil {
					return nil, err
				}
				examples = append(examples, string(exampleBytes))
			}
		}

		fieldCtx := fieldContext{
			Name:             v.Name,
			Type:             string(fieldType),
			Description:      v.Description,
			Default:          defaultValueStr,
			Advanced:         v.Advanced,
			Examples:         examples,
			AnnotatedOptions: v.AnnotatedOptions,
			Options:          v.Options,
			Interpolation:    v.Interpolation,
			Version:          v.Version,
		}

		if len(fieldCtx.Description) == 0 {
			fieldCtx.Description = "Sorry! This field is missing documentation."
		}

		if fieldCtx.Description[0] == '\n' {
			fieldCtx.Description = fieldCtx.Description[1:]
		}

		ctx.Fields = append(ctx.Fields, fieldCtx)
	}

	var buf bytes.Buffer
	err := template.Must(template.New("component").Parse(componentTemplate)).Execute(&buf, ctx)

	return buf.Bytes(), err
}
